06. 조건 분기: 미궁처럼 복잡한 분기 처리를 무너뜨리는 방법

📌 Contents

📌 조건 분기가 중첩되어 낮아지는 가독성

  • if 조건문을 중첩하면 코드의 가독성이 크게 떨어짐
  • 어디부터 어디까지가 if 조건문의 처리 블록(중괄호({})로 감싸진 처리 범위)인지 이해하기 힘듬
  • 중첩 구조 사이사이에 수많은 코드가 섞여 있으면, if 조건문의 범위({와 }사이)를 찾기 매우 힘듬

조기 리턴으로 중첩 제거하기

  • 중첩 악마를 퇴치하는 방법 중 하나로 조기 리턴(early return)이 있음
  • 조기 리턴은 조건을 만족하지 않는 경우 곧바로 리턴하는 방법
  • 조기 리턴하는 형태로 변경하려면, 원래 조건을 반전해야 함
  • 조기 리턴을 하면 중첩이 제거되어 가독성이 좋아짐
  • 또 조건 로직과 실행 로직을 분리 할 수 있어 좋음

가독성을 낮추는 else 구문도 조기 리턴으로 해결하기

  • else 구문도 가독성을 나쁘게 만드는 원인임
  • if-else 구문에 안에 return을 넣으면 else를 쓸 필요 없이 if만 사용하도록 로직을 개선할 수 있음

📌 switch 조건문 중복

  • 값의 종류에 따라 다르게 처리하고 싶을 때는 switch 조건문을 많이 사용함
  • 하지만 switch 조건문은 악마를 불러들이기 굉장히 쉬운 제어 구문임

switch 조건문의 악마

  • 값의 종류에 따라 switch 조건문을 사용함
  • 종류들 중 하나에 처리해야 할 대상이 여러 개라면, 같은 형태의 switch 조건문이 여러 개 사용될 수 있음
  • 그런 구조라면 요구 사항 변경 시 수정이 누락될 수 있음 (case 구문 추가 누락)
  • 예를 들어 마법(특정 값)의 종류가 3가지(파이어, 라이트닝, 헬파이어)가 있고 그 중 한 가지가 처리해야 하는 대상이 4개(이름, mp 소비량, 공격력, tp 소비량)라면 같은 형태(case가 3개인 형태)의 switch 조건문이 4개가 생길 것임
  • 이 때 마법의 종류가 하나가 추가가 된다면 switch문 4개 모두에 추가된 마법(case)를 추가해 줘야하는데, swtich 문이 4개나 되다보니 추가하는 것을 깜빡할 수 있음
  • 이렇게 된다면 새로 추가된 종류는 처리해야 하는 대상을 모두 처리 못하게 될 수 있음
  • 이렇게 switch 조건문이 많이 중복된다면, 수정이 누락되어 버그가 발생할 수 있고, 가독성이 떨어짐

조건 분기 모으기

  • switch 조건문 중복을 해소하려면, 단일 책임 선택의 원칙을 생각해 봐야함
    • 단일 책임 선택의 원칙: 소프트웨어 시스템이 선택지를 제공해야 한다면, 그 시스템 내부의 어떤 한 모듈만으로 모든 선택지를 파악할 수 있어야 한다.
    • 간단하게 말해, 조건식이 같은 조건 분기를 여러 번 작성하지 말고 한 번에 작성하자는 뜻
  • 단일 책임 선택의 원칙에 따라 값의 종류에 따른 switch 조건문을 하나로 묶는다면, 즉 처리해야할 대상을 하나의 switch문에서 모두 처리한다면 누락 실수를 줄일 수 있음

인터페이스로 switch 조건문 중복 해소하기

  • 단일 책임 선택의 원칙으로 switch 조건문을 하나만 사용하는데, 변경하고 싶은 부분이 많아지면, 클래스가 거대해져서 데이터와 로직의 관계를 알기 힘들어짐
  • 따라서 클래스가 거대해지면 관심사에 따라 작은 클래스들로 분할하는 것이 중요한데, 이 때 '인터페이스'를 사용함
  • 인터페이스는 객체 지향 프로그래밍 언어의 특유의 문법으로, 기능 변경을 편리하게 할 수 있는 개념
  • 인터페이스를 사용하면, 분기 로직을 작성하지 않고도 분기와 같은 기능을 구현할 수 있음
  • 잘 활용하면 조건 분기가 많이 줄어들어 로직이 간단해짐


  • 인터페이스의 사용 예시로 사각형 클래스와 원 클래스를 들어보자
  • 각각의 클래스에는 면적을 구하는 area() 메서드가 있다
  • 이런 상황에서
    Rectangle rectangle = new Circle(8);
    rectangle.area();
    
    이런 코드를 작성한다면, 다른 자료형의 인스턴스는 할당할 수 없어 컴파일 오류가 발생
    또한 같은 이름의 메서드라도 사용할 수 없음
  • 면적을 출력하는 같은 이름의 메서드를 만들었다고 해도 클래스가 다르면, instanceof를 사용해서 자료형을 판정한 뒤, 자료형을 강제로 변환해서 메서드를 호출해야 함
  • 이러한 번거러움을 해결해 주는 것이 인터페이스임
  • 인터페이스는 서로 다른 자료형을 같은 자료형처럼 사용할 수 있게 해줌
  • Shape이라는 인터페이스를 만든 예제

    interface Shape {
      double area();
    }
    
    // 사각형
    class Rectangle implements Shape {
      ...
    }
    
    // 원
    class Circle implements Shape {
      ...
    }
    
    // Shape 인터페이스를 구현하는 Rectangle, Circle 모두를 할당할 수 있음.
    Shape shape = new Circle(10);
    System.out.println(shape.area()); // 원의 면적 출력
    shape = new Rectangle(25);
    System.out.println(shape.area()); // 사각형의의 면적 출력
    

    이렇게 하면 Rectangle과 Circle을 Shape라는 자료형으로 다룰 수 있음
    같은 자료형으로 사용할 수 있으므로, 굳이 자료형을 판단하지 않아도 됨
    즉, 인터페이스를 활용하면, 자료형 판정 분기를 따로 작성하지 않아도 됨

인터페이스를 switch 조건문 중복에 응용하기(전략 패턴)

1. 종류별로 다르게 처리해야 하는 기능을 인터페이스의 메서드로 정의하기
  • 앞에서 언급한 마법 예시에서, switch 조건문을 사용해 마법의 이름, mp 소비량, 공격력, tp 소비량을 다르게 처리함
  • 이처럼 다르게 처리하고 싶은 기능을 인터페이스의 메서드로 정의
2. 인터페이스의 이름을 결정하는 방법: '어떤 부류에 속하는가?'
  • 인터페이스의 이름을 결정하는 방법은 여러 가지가 있음
  • 가장 기본적인 방법은 '인터페이스를 구현하는 클래스들이 어떤 부류인가?'를 생각해 보는 것
  • 파이어, 라이트닝, 헬파이어는 마법이라는 부류이므로 인터페이스 이름을 Magic으로 하면 됨
3. 종류별 클래스로 만들기
  • 마법의 종류들을 각각의 클래스로 만들기
  • Fire, Lightning, HellFire 클래스를 만들면 됨

4. 각각의 클래스에 인터페이스 구현하기

  • 각 마법 클래스에서 Magic 인터페이스를 구현
  • 예를 들어 Fire 클래스에서 이름, mp 소비량, 공격력, tp 소비량을 확인할 수 있게 각각의 메서드를 구현
    class Fire implements Magic { ... }
  • 이와 같이 구현하면, Fire, Lightning, HellFire를 모두 Magic 자료형으로 활용 가능
5. switch 조건문이 아니라, Map으로 변경하기
  • Map을 만들고 키를 enum MagicType으로 지정하고, 값을 Magic 인터페이스 구현 클래스의 인스턴스로 지정하면,
  • Map에서 전달받은 MagicType에 대응하는 인스턴스를 추출해 그 인스턴스에 대한 메서드를 처리할 수 있음
  • Map이 switch 조건문처럼 경우에 따라 처리를 구분하는 것임
  • switch 조건문을 전혀 사용하지 않고, 마법별로 처리를 나눔
  • magics.get(magicType)(Map 활용)으로 모두 한꺼번에 전환하고 있는 점이 특징
  • 처리별로 switch 조건문을 사용하는 분기 자체를 사용하지 않음
  • 이처럼 인터페이스를 사용해서 처리를 한꺼번에 전환하는 설게를 '전략 패턴'이라고 함
6. 메서드를 구현하지 않으면 오류로 인식하게 만들기
  • 인터페이스를 활용한 전략 패턴은 switch 조건문을 사용하지 않을 수 있다는 것 이외에도 여러 장점이 있음
  • 인터페이스에 구현된 메서드(처리하고 싶은 기능)를 클래스에 구현하지 않는다면 컴파일 오류가 발생하게 됨
  • 따라서 마법(case)을 추가할 때 처리하고 싶은 기능(메서드)를 구현하지 않는다는 실수 자체를 방지할 수 있음 (수정 누락 방지)
7. 값 객체화 하기
  • 마지막으로 코드의 품질을 더욱 향상시키려면, 메서드들도 값 객체로 만들어서 리턴 값을 기본 자료형이 아니게 만들기
  • 이름, mp 소비량, 공격력, tp 소비량 메서드들이 모두 String이나 int와 같은 기본 자료형을 리턴하기 보다는 값 객체로 만들 수 있다면 값 객체로 만들어 기본 자료형 사용을 줄이는 것이 좋음

📌 조건 분기 중복과 중첩

  • 인터페이스는 switch 조건문의 중복을 제거할 수 있을 뿐만 아니라, 다중 중첩된 복잡한 분기를 제거하는 데 활용할 수 있음
  • 골드 회원인지 판단하는 메서드와 실버 회원이지 판단하는 메서드는 2개의 조건이 같고 골드 회원이 조건이 하나 더 있음
  • 골드, 실버 외 브론즈 등이 더 추가 되고 추가된 등급도 기존의 등급과 비슷한 판정 조건을 갖게 되면 같은 판정 로직이 여러 곳에 작성될 것임


  • 이러한 상황에서 유용하게 활용할 수 있는 패턴은 정책 패턴(policy pattern)임
  • 조건을 부품처럼 만들고, 부품을 만든 조건을 조합해서 사용하는 패턴
  • 일단 하나하나의 규칙(판정 조건)을 나타내는 인터페이스를 만듦
  • 그 다음 정책 클래스를 만들고, 정책 클래스 안에서 add 메서드를 통해 하나하나 따로 만든 규칙을 넣어줌
  • 그 다음 정책 클래스 안에서 complyWithAll 메서드를 통해 규칙을 모두 만족하는지 판정함
class GoldCustomerPolicy {
  private final ExcellentCustomerPolicy policy;

  GoldCustomerPolicy() {
    policy = new ExcellentCustomerPolicy();
    policy.add(new GoldCustomerPurchaseAmountRule());
    policy.add(new PurchaseFrequencyRule());
    policy.add(new ReturnRateRule());
  }

  /**
  * @param history 구매 이력
  * @return 규칙을 모두 만족하는 경우 true
  */
  boolean complyWithAll(final PurchaseHistory history) {
    return policy.complyWithAll(history);
  }
}
  • 골드 회원 조건이 집약된 클래스 구조
  • 골드 회원 조건이 달라질 경우, 이 클래스만 변경하면 됨
  • 다른 회원들도 같은 방법으로 만들면, 규칙이 재사용되고 있으므로, 멀리 내다보았을 때도 괜찮은 클래스 구조임

📌 자료형 확인에 조건 분기 사용하지 않기

  • 인터페이스를 사용해도 조건 분기가 줄어들지 않는 경우가 있음
  • instanceof를 활용해 인터페이스 구현 클래스의 자료형을 확인해서 분기하는 경우
    • 일반 객실 요금과, 프리미엄 객실 요금에 성수기 요금을 추가할 때, instanceof를 활용해 일반 객실 요금 자료형인지, 프리미엄 객실 요금 자료형인지 if문으로 판정 후 판정된 것에 맞게 추가해 줘야함
    • 특정 기간에 적용하는 요금을 추가한다면, 조건 분기가 계속 많아짐
  • 이와 같은 로직은 리스코프 치환 원칙이라는 소프트웨어 원칙을 위반함
    • 리스코프 원칙은 클래스의 기반 자료형과 하위 자료형 사이에 성립하는 규칙
    • 간단하게, '기반 자료형을 하위 자료형으로 변경해도, 코드는 문제없이 동작해야 한다'라는 의미
    • 여기서 기반 자료형은 인터페이스, 하위 자료형은 인터페이스를 구현한 클래스를 의미함
  • 리스코프 치환 원칙을 위반하면 자료형 판정을 위한 조건 분기 코드가 점점 많아져서, 유지 보수하기 어려운 코드가 됨
  • 인터페이스의 의미를 충분히 이해하지 못하고 사용하면 이와 같은 로직이 자주 만들어짐
// instanceof로 조건 분기 하는 버전
interface HotelRates {
  Money fee(); // 요금
}

class RegularRates implements hotelRates {
  public Money fee() {
    return new Money(70000);
  }
}

class PremiumRates implements hotelRates {
  public Money fee() {
    return new Money(120000);
  }
}

Money busySeasonFee;
if (hotelRates instanceof RegularRates) {
  busySeasonFee = hotelRates.fee().add(new Money (30000));
}
else if (hotelRates instanceof PremiumRates) {
  busySeasonFee = hotelRates.fee().add(new Money (50000));
}

// instanceof로 조건 분기를 없애 버전
interface HotelRates {
  Money fee();
  Money busySeasonFee(); // 성수기 요금
}

class RegularRates implements hotelRates {
  public Money fee() {
    return new Money(70000);
  }

  public Money busySeasonFee() {
    return fee().add(new Money(30000));
  }
}

class PremiumRates implements hotelRates {
  public Money fee() {
    return new Money(120000);
  }

  public Money busySeasonFee() {
    return fee().add(new Money(50000));
  }
}

Money busySeasonFee = hotelRates.busySeasonFee();

📌 인터페이스 사용 능력이 중급으로 올라가는 첫걸음

  • 인터페이스를 잘 사용하면 조건 분기를 크게 줄일 수 있음
  • 코드를 단순하게 만들 수 있음
  • 인터페이스를 잘 사용하는지가 곧 설계 능력의 전환점이라고 할 수 있음
  • 조건 분기를 써야 하는 상황에는 일단 인터페이스 설계를 떠올리자!
  • 를 머릿속에 새겨 두기만 해도 조건 분기 처리를 대하는 방식 자체가 달라질 것임

📌 플래그 매개변수

  • 메서드의 기능을 전환하는 boolean 자료형의 매개변수를 플래그 매개변수라고 함
  • 플래그 매개변수를 받는 메서드는 어떤 일을 하는지 예측하기 굉장히 힘듬
  • 예측을 하기 위해서는 반드시 메서드 내부 로직을 확인해야 하므로, 가독성이 낮아지며 개발 생산성이 저하됨
  • boolean 자료형뿐만 아니라, int 자료형을 사용해 기능을 전환하는 경우도 같은 문제 발생

메서드 분리하기

  • 플래그 매개변수를 받는 메서드는 내부적으로 여러 기능을 수행하고 있으며, 플래그를 사용해서 이를 전환하는 구조를 갖음
  • 메서드는 하나의 기능만 설계하는 것이 좋기 때문에, 플래그 매개변수를 받는 메서드는 기능별로 분리하는 것이 좋음
  • 메서드를 기능별로 나누고, 각각의 메서드에 맞는 이름을 붙이면 가독성이 높아짐

전환은 전략 패턴으로 구현하기

  • 메서드를 기능별로 분할해도 전환이 필요할 수 있음
  • 전환을 위해 boolean 자료형을 사용하면, 플래그 매개변수로 돌아가는 것임
  • 플래그 매개변수가 아닌 전략 패턴을 사용하자!

❓ Questions

❓ 인터페이스의 구현

  • 원래 '인터페이스를 구현한다'라는 말은 인터페이스를 정의하는 것이라고 생각했음
  • 정의된 인터페이스를 클래스에 적용하는 것을 '인터페이스를 구현한다'라고 하는 것 같음

❓ implements, extends

  • 인터페이스를 클래스에 구현할 때 implements를 사용함
  • implements는 구현이라는 뜻으로 기존에 알고 있던 클래스를 상속해주는 extends와 비슷한 역할을 하는 것 같음
  • 그럼 implements와 extends의 차이점은 뭘까?


  • extends는 부모에서 메서드를 선언하고 정의까지 해서 자식은 메서드를 오버라이딩(재정의)할 필요 없이 그대로 사용 가능하다.
  • 하지만 implements는 부모에서 선언만 하고 자식에서 오버라이딩(재정의)를 해서 사용해야 한다.
  • extends는 다중 상속을 지원하지 않고 implements는 지원한다.
  • 따라서 다중 상속을 위해서는 implements를 사용해야한다.
  • implements는 근데 메서드를 무조건 재정의 해야하기 때문에 상속이라고 할 수 있나 싶지만, 자바에서는 이것도 상속이라고 보는 것 같다.
  • extends는 일반 클래스와 추상 클래스 상속에 사용되고, implements는 인터페이스 상속(구현)에 사용 된다.
  • 인터페이스가 인터페이스를 상속받을 땐 extends를 사용한다.

❓ 인터페이스의 의미란?

  • 책에서 인터페이스의 의미를 충분히 이해하지 못하고 사용하면 자료형을 판정하는 조건 분기가 많아질 수 있다고 했음
  • 여기서 말하는 인터페이스의 의미가 무엇일까?
  • 인터페이스의 의미를 충분히 이해하면 자료형을 판정하는 조건 분기를 줄일 수 있는 이유가 무엇일까?


  • 내가 생각하기에 인터페이스는 관련된 로직을 모두 내부에 담고 있어야 한다고 생각한다.
  • 책에서 예시로 나온 호텔 객실 요금을 보면, 처음에는 성수기 요금을 계산하는 로직이 인터페이스에 들어있지 않아 성수기 요금을 추가할 때 외부에서 호텔 객실 요금이 일반인지, 프리미엄인지에 따라(자료형에 따라) 조건 분기를 해서 성수기 요금을 추가해 주었다.
  • 하지만 인터페이스의 의미를 충분히 이해해서 성수기 요금을 추가하는 로직을 인터페이스 내부에 추가해주었다면, 외부에서 조건 분기를 통해 성수기 요금을 추가할 필요 없이 클래스 내부에서 성수기 요금을 추가해 줄 수 있게 된다.
  • 위에서 책의 예시를 본다면 어떤식으로 구현했는지 알 수 있다.
  • 따라서 책에서 말한 인터페이스의 의미는 관련된 로직을 담아둔다는 의미가 맞는 것 같다.

results matching ""

    No results matching ""